﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Reflection;
using System.Xml.Linq;

namespace System.Xml
{
	public class XmlSerializeProvider
	{
		private const string XML_ID_ATTR_OBJ = "xml.refSer-obj";		

		/// <summary>
		/// Сериализация объекта в XElement
		/// </summary>
		/// <param name="subj">Объект</param>
		/// <param name="sc"></param>
		/// <returns>Возвращаемый элемент имеет имя 'Element' и атрибут type с наименованием типа (для классов-прототипов имя генерится с треугольными скобками, как в синтаксисе C#).</returns>
		public static XElement Serialize(object subj)
		{
			return Serialize(subj, false, null, false);
		}

		public static XElement Serialize(object subj, bool? asRef)
		{
			return Serialize(subj, false, null, asRef);
		}

		/// <summary>
		/// Сериализация объекта в XElement
		/// НЕ вызывать из метода ((ISerialize)subj).Serialize()!!!
		/// Используй <seealso cref="SerializeOnlyPropertyAndType"/>
		/// </summary>
		/// <param name="subj">Объект</param>
		/// <param name="elementName">Имя выходного элемента. Тип будет записан в атрибут type.</param>
		/// <returns></returns>
		public static XElement Serialize(object subj, XName elementName)
		{
			return Serialize(subj, elementName, null, false);
		}

		public static XElement Serialize(object subj, XName elementName, bool? asRef)
		{
			return Serialize(subj, elementName, null, asRef);
		}

		public static XElement Serialize(object subj, Type typeifNull)
		{
			bool addTypeName = subj != null && typeifNull != null && subj.GetType() != typeifNull;
			return Serialize(subj, addTypeName, typeifNull, false);
		}

		public static XElement Serialize(object subj, Type typeifNull, bool? asRef)
		{
			bool addTypeName = subj != null && typeifNull != null && subj.GetType() != typeifNull;
			return Serialize(subj, addTypeName, typeifNull, asRef);
		}

		/// <summary>
		/// Сериализация объекта в XElement
		/// </summary>
		/// <param name="subj"></param>
		/// <param name="elementName"></param>
		/// <param name="typeifNull"></param>
		/// <returns></returns>
		public static XElement Serialize(object subj, XName elementName, Type typeifNull)
		{
			XElement xe = Serialize(subj, typeifNull);
			xe.Name = elementName;
			return xe;
		}

		public static XElement Serialize(object subj, XName elementName, Type typeifNull, bool? asRef)
		{
			XElement xe = Serialize(subj, typeifNull, asRef);
			xe.Name = elementName;
			return xe;
		}

		/// <summary>
		/// Сериализация объекта в XElement.
		/// НЕ вызывать из метода ((ISerialize)subj).Serialize()!!!
		/// Используй <seealso cref="SerializeOnlyPropertyAndType"/>
		/// </summary>
		/// <param name="subj">Объект</param>
		/// <param name="saveTypeNameToAttribute">true - сохранение имени класса в атрибуте type, иначе в имени элемента.</param>
		/// <param name="typeifNull">Тип используемый если объект == null</param>
		/// <returns>Возвращаемый элемент имеет имя 'Element' и атрибут type с наименованием типа (для классов-прототипов имя генерится с треугольными скобками, как в синтаксисе C#).</returns>
		public static XElement Serialize(object subj, bool saveTypeNameToAttribute, Type typeifNull, bool? asRef)
		{
			var ser = subj as ISerialize;
			string nm;
			if (subj != null)
				nm = subj.GetType().GetGenericName();
			else if (typeifNull != null)
				nm = typeifNull.GetGenericName();
			else
				return null;
			if (nm.Contains('['))
				saveTypeNameToAttribute = true;
			if (ser != null)
			{
				var xe = ser.Serialize();
				if (saveTypeNameToAttribute)
					xe.SetAttributeValue("type", nm);
				else
					xe.Name = xe.Name.Namespace + nm;
				return xe;
			}
			else
				return transform(nm, subj, saveTypeNameToAttribute, typeifNull, asRef);
		}

		/// <summary>
		/// Сериализация только свойств и типа объекта, без попытки приведения к ISerialize
		/// </summary>
		/// <param name="subj">Объект</param>
		/// <param name="saveTypeNameToAttribute">true - сохранение имени класса в атрибуте type, иначе в имени элемента.</param>
		/// <returns></returns>
		public static XElement SerializeOnlyPropertyAndType(object subj, bool saveTypeNameToAttribute)
		{
			return transform(subj.GetType().GetGenericName(), subj, saveTypeNameToAttribute, null, false);
		}

		public static XElement SerializeOnlyPropertyAndType(object subj, bool saveTypeNameToAttribute, Type typeIfNull)
		{
			return transform(subj.GetType().GetGenericName(), subj, saveTypeNameToAttribute, typeIfNull, false);
		}

		private static IEnumerable<KeyValuePair<PropertyInfo, SerializeAttribute>> GetPropsInfo(Type type)
		{
			return cacheProps.GetOrAdd(type, () => type.GetPropertiesAndAttribute<SerializeAttribute>(true));
		}

		private static XElement transform(string nameType, object subj, bool saveTypeNameToAttribute, Type typeIfNull, bool? asRef)
		{
#if DEBUG
			DescriptionAttribute da;
#endif
			string nm = nameType;
			object value;
			bool hasSpecProps = false;
			if (nm.Contains('['))
				saveTypeNameToAttribute = true;
			Type tp = ((subj != null) ? subj.GetType() : typeIfNull);
			if (tp == null)
				return null;
			XElement xe = new XElement(saveTypeNameToAttribute ? "Element" : nm,
				saveTypeNameToAttribute ? new XAttribute("type", nm) : null), x;
			if (!tp.IsValueType)
				if (asRef.HasValue && asRef.Value || !asRef.HasValue && SerializeScope.IsPresentObject(subj))
				{
					xe.Add(SerializeScope.Add(subj));
				}
				else
				{
					var props = GetPropsInfo(tp);

					ISerialize sr;
					if (subj != null)
					{
						if (tp != typeof(string))
							SerializeScope.Add(subj, (obj, id) => xe.SetAttributeValue(XML_ID_ATTR_OBJ, id));
						foreach (var item in props)
						{
							hasSpecProps = true;
							value = item.Key.GetValue(subj, TypeExtension.nullPrms);
							sr = value as ISerialize;
							if (!value.IsDefault() || !item.Value.IgnoreDefault)
								switch (item.Value.SerializeAs)
								{
									case SerializeAs.Value:										
										xe.Add(value);
										break;
									case SerializeAs.Element:
#if DEBUG
										if ((da = item.Key.GetAttribute<DescriptionAttribute>()) != null)
											xe.Add(new XComment(da.Description));
#endif
										if (item.Value.AsReference)
										{
											if (SerializeScope.HasScope)
												xe.Add(new XElement(item.Key.Name, SerializeScope.Add(value)));
										}
										else
											xe.Add(Serialize(value, item.Key.Name, item.Key.PropertyType));
										break;
									case SerializeAs.Attribute:
										if (item.Value.AsReference)
										{
											if (SerializeScope.HasScope)
												xe.Add(new XAttribute(item.Key.Name, SerializeScope.Add(value)));
										}
										else
											xe.Add(new XAttribute(item.Key.Name, value ?? ""));
										break;
									case SerializeAs.Collection:
										if (sr != null)
											xe.Add(Serialize(value, item.Key.Name, item.Key.PropertyType));
										else
										{
											xe.Add(x = Serialize(value, item.Key.Name, item.Key.PropertyType));
											x.Add(GetItemsCollection(value, item.Value.TypeItem));
										}
										break;
								}

						}
					}
				}
			if (!hasSpecProps && (op_XElementConvert.ContainsKey(tp) || tp.IsEnum))
				xe.Add(subj);
			return xe;
		}

		private static IEnumerable<XElement> GetItemsCollection(object coll, Type typeCollection)
		{
			ISerialize ser;
			if (coll != null)
				if (typeof(ISerialize).IsAssignableFrom(typeCollection))
					foreach (var item in ((IEnumerable)coll).OfType<ISerialize>())
						yield return item.Serialize();
				else
					foreach (var item in ((IEnumerable)coll))
						if ((ser = item as ISerialize) != null)
							yield return ser.Serialize();
						else
							yield return Serialize(item, false, typeCollection, true);
		}

		/// <summary>
		/// Десериализация объекта (класс объекта должен иметь public-конструктор без параметров).
		/// Xml-элемент должен содержать атрибут type, в котором указано имя класса (для классов-прототипов имя с треугольными скобками, как в синтаксисе C#), иначе имя класс можно взять из имени элемента.
		/// </summary>
		/// <typeparam name="T">Класс к которому приводится полученный объект.</typeparam>
		/// <param name="xe">Xml-элемент, содержащий сериализованный объект.</param>        
		/// <returns>Приведенный объект</returns>
		/// <exception cref="Exception">Вызывается в случае неверного имени типа или отсутствия public-конструктора без параметров. В поле Exception.Message описание ошибки.</exception>
		public static T Deserialize<T>(XElement xe)
			where T : class
		{
			return (T)Deserialize(xe, typeof(T));
		}

		public static void Deserialize<T>(XElement xe, bool? asRef, Action<object> lazyLoad)
			where T : class
		{
			Deserialize(xe, asRef, lazyLoad, typeof(T));
		}

		/// <summary>
		/// Десериализация значения из XElement'а
		/// </summary>
		/// <typeparam name="T"></typeparam>
		/// <param name="xe"></param>
		/// <returns></returns>
		public static T DeserializeValue<T>(XElement xe)
		{
			object res;
			TryConvertOrDeserialize(xe, typeof(T), out res);
			return (T)res;
		}

		/// <summary>
		/// Десериализация объекта (класс объекта должен иметь public-конструктор без параметров).
		/// Xml-элемент должен содержать атрибут type, в котором указано имя класса (для классов-прототипов имя с треугольными скобками, как в синтаксисе C#), иначе имя класс можно взять из имени элемента.
		/// </summary>
		/// <typeparam name="T">Класс к которому приводится полученный объект.</typeparam>
		/// <param name="xe">Xml-элемент, содержащий сериализованный объект.</param>        
		/// <returns>Приведенный объект</returns>
		/// <exception cref="Exception">Вызывается в случае неверного имени типа или отсутствия public-конструктора без параметров. В поле Exception.Message описание ошибки.</exception>
		public static object Deserialize(XElement xe, params Type[] compatibles)
		{
			object subj = CreateInstance(xe, compatibles);
			var ser = subj as ISerialize;
			if ((subj != null).IfFalse(() => new Exception("Не удалось создать экземпляр типа {0}".goFormat(GetTypeName(xe)))))
				if (ser != null)
					ser.SetUp(xe);
				else
					SetUp(subj, xe);
			return subj;
		}

		public static void Deserialize(XElement xe, bool? asRef, Action<object> lazyLoad, params Type[] compatibles)
		{
			long? id;
			try
			{
				id = (long?)xe;
			}
			catch
			{
				id = null;
			}
			if (asRef.HasValue && !asRef.Value || !asRef.HasValue && (!id.HasValue || id.Value == 0))
			{
				if (lazyLoad != null)
					lazyLoad(Deserialize(xe, compatibles));
			}
			else if (!asRef.HasValue || asRef.Value)
				deserializeAsRef(id, lazyLoad);
		}

		public static bool DeserializeAsRef(XElement xe, Action<object> lazyLoad)
		{
			return deserializeAsRef((long?)xe, lazyLoad);
		}

		public static bool DeserializeAsRef(XAttribute xa, Action<object> lazyLoad)
		{
			return deserializeAsRef((long?)xa, lazyLoad);
		}

		private static bool deserializeAsRef(long? id, Action<object> lazyLoad)
		{
			bool res = SerializeScope.HasScope
						&& id.HasValue
						&& id.Value != 0;
			if (res)
				SerializeScope.GetOrSuspend(id.Value, lazyLoad);
			return res && SerializeScope.IsPresentID(id.Value);
		}

		public static bool TryDeserialize(XElement xe, out object res, params Type[] compatibles)
		{
			try
			{
				res = Deserialize(xe, compatibles);
			}
			catch
			{
				res = null;
				return false;
			}
			return true;
		}

		private static string GetTypeName(XElement xe, bool nameIsElementName)
		{
			return !nameIsElementName ? (string)xe.Attribute("type") : xe.Name.LocalName;
		}

		private static string GetTypeName(XElement xe)
		{
			return GetTypeName(xe, xe.Attribute("type") == null);
		}

		/// <summary>
		/// Инстанцирование объекта (класс объекта должен иметь public-конструктор без параметров).
		/// Xml-элемент должен содержать атрибут type, в котором указано имя класса (для классов-прототипов имя с треугольными скобками, как в синтаксисе C#), иначе имя класс будет взято из имени элемента.
		/// </summary>
		/// <typeparam name="T">Класс к которому приводится полученный объект.</typeparam>
		/// <param name="xe">Xml-элемент, содержащий сериализованный объект.</param>
		/// <returns>Вызывается в случае неверного имени типа или отсутствия public-конструктора без параметров. В поле Exception.Message описание ошибки.</returns>
		public static T CreateInstance<T>(XElement xe)
			where T : class
		{
			return CreateInstance<T>(xe, xe.Attribute("type") == null);
		}

		/// <summary>
		/// Инстанцирование объекта (класс объекта должен иметь public-конструктор без параметров).        
		/// </summary>
		/// <typeparam name="T">Класс к которому приводится полученный объект.</typeparam>
		/// <param name="xe">Xml-элемент, содержащий сериализованный объект.</param>
		/// <param name="nameIsElementName">true - имя класса будет взято из имени элемента, иначе из атрибута type.</param>
		/// <returns>Вызывается в случае неверного имени типа или отсутствия public-конструктора без параметров. В поле Exception.Message описание ошибки.</returns>
		public static T CreateInstance<T>(XElement xe, bool nameIsElementName)
			where T : class
		{
			return (T)CreateInstance(xe, nameIsElementName, typeof(T));
		}

		/// <summary>
		/// Инстанцирование объекта (класс объекта должен иметь public-конструктор без параметров).        
		/// </summary>
		/// <typeparam name="T">Класс к которому приводится полученный объект.</typeparam>
		/// <param name="xe">Xml-элемент, содержащий сериализованный объект.</param>
		/// <param name="nameIsElementName">true - имя класса будет взято из имени элемента, иначе из атрибута type.</param>
		/// <param name="compatibles">Ограничение по совместимости</param>
		/// <returns>Вызывается в случае неверного имени типа или отсутствия public-конструктора без параметров. В поле Exception.Message описание ошибки.</returns>
		public static object CreateInstance(XElement xe, bool nameIsElementName, params Type[] compatibles)
		{
			Type t = TypeExtension.GetTypeFromNameCompatible(
				GetTypeName(xe, nameIsElementName),
				(types) => types.FirstOrDefault(s => !s.IsAbstract && !s.IsInterface)
				, compatibles);
			object subj = null;
			if (!TryConvert(xe, t, out subj))
				subj = t.CreateInstance();
			return subj;
		}

		/// <summary>
		/// Инстанцирование объекта (класс объекта должен иметь public-конструктор без параметров).
		/// Xml-элемент должен содержать атрибут type, в котором указано имя класса (для классов-прототипов имя с треугольными скобками, как в синтаксисе C#), иначе имя класс будет взято из имени элемента.
		/// </summary>
		/// <typeparam name="T">Класс к которому приводится полученный объект.</typeparam>
		/// <param name="xe">Xml-элемент, содержащий сериализованный объект.</param>
		/// <param name="compatibles">Ограничение по совместимости</param>
		/// <returns>Вызывается в случае неверного имени типа или отсутствия public-конструктора без параметров. В поле Exception.Message описание ошибки.</returns>
		public static object CreateInstance(XElement xe, params Type[] compatibles)
		{
			return CreateInstance(xe, xe.Attribute("type") == null, compatibles);
		}

		/// <summary>
		/// Настройка объекта
		/// </summary>
		/// <param name="subj">Объект</param>
		/// <param name="xe">Xml-элемент, содержащий сериализованный объект (или его часть, например свойства).</param>
		public static void SetUp(object subj, XElement xe)
		{
			XElement x;
			long? id;
			if (subj != null)
			{
				var props = GetPropsInfo(subj.GetType());
				foreach (var item in props)
				{
					object val;
					switch (item.Value.SerializeAs)
					{
						case SerializeAs.Collection:
							if ((x = xe.Element(item.Key.Name)) != null)
							{
								var typeitem = item.Value.TypeItem;
								var coll = item.Key.GetValue(subj, TypeExtension.nullPrms);
								var createdColl = coll == null;
								if (createdColl)
									coll = CreateInstance(x, item.Key.PropertyType);
								if (coll != null)
								{
									cacheAddItemsToCollectionT
										.GetOrAddDefault(typeitem, CacheAddItemsToCollectionT(typeitem))
										.Invoke(null, new[] { coll, x.Elements() });
									if (createdColl)
										item.Key.SetValue(subj, coll, TypeExtension.nullPrms);
								}
							}
							break;
						case SerializeAs.Value:
							if (TryConvertOrDeserialize(xe, item.Key.PropertyType, out val))
								item.Key.SetValue(subj, val, TypeExtension.nullPrms);
							break;
						case SerializeAs.Element:
							x = xe.Element(item.Key.Name);
							if (x != null)
								if (item.Value.AsReference)
									DeserializeAsRef(x, LazySetValue(subj, item.Key));
								else if (TryConvertOrDeserialize(x, item.Key.PropertyType, out val))
									item.Key.SetValue(subj, val, TypeExtension.nullPrms);
							break;
						case SerializeAs.Attribute:
							XAttribute xa = xe.Attribute(item.Key.Name);
							if (xa != null)
								if (item.Value.AsReference)
									DeserializeAsRef(xa, LazySetValue(subj, item.Key));
								else if (TryConvert(xa, item.Key.PropertyType, out val))
									item.Key.SetValue(subj, val, TypeExtension.nullPrms);
							break;
					}
				}
				if (SerializeScope.HasScope)
					if ((id = (long?)xe.Attribute(XML_ID_ATTR_OBJ)).HasValue && id.Value != 0)
						SerializeScope.Add(id.Value, subj);
					else
						SerializeScope.Add(subj);
			}
		}

		private static Func<MethodInfo> CacheAddItemsToCollectionT(Type tp)
		{
			return () => AddItemsToCollectionT.MakeGenericMethod(tp);
		}

		private static Action<object> LazySetValue(object subj, PropertyInfo pi)
		{
			return o => pi.SetValue(subj, o, TypeExtension.nullPrms);
		}

		private static void AddItemsToCollection<T>(object coll, IEnumerable<XElement> elements, SerializeScope ss)
		{
			var c = coll as ICollection<T>;
			if (c != null)
				foreach (var item in elements.Select(s => Deserialize(s, typeof(T))).Cast<T>())
					c.Add(item);
		}


		private static readonly Dictionary<Type, MethodInfo> cacheAddItemsToCollectionT
			= new Dictionary<Type, MethodInfo>();
		private static readonly Dictionary<Type, MethodInfo> op_XElementConvert =
			new Dictionary<Type, MethodInfo>();
		private static readonly Dictionary<Type, MethodInfo> op_XAttributeConvert =
			new Dictionary<Type, MethodInfo>();
		private static readonly Dictionary<Type, MethodInfo> op_TryParse =
			new Dictionary<Type, MethodInfo>();
		private static readonly DictionaryList<Type, KeyValuePair<PropertyInfo, SerializeAttribute>> cacheProps =
			new DictionaryList<Type, KeyValuePair<PropertyInfo, SerializeAttribute>>();

		static XmlSerializeProvider()
		{
			foreach (var item in typeof(XElement).GetMethods().Where(s => s.Name == "op_Explicit" && s.IsSpecialName && s.IsStatic && !op_XElementConvert.ContainsValue(s)))
				op_XElementConvert.Add(item.ReturnType, item);

			foreach (var item in typeof(XAttribute).GetMethods().Where(s => s.Name == "op_Explicit" && s.IsSpecialName && s.IsStatic && !op_XElementConvert.ContainsValue(s)))
				op_XAttributeConvert.Add(item.ReturnType, item);
			AddItemsToCollectionT = typeof(XmlSerializeProvider).GetMethod("AddItemsToCollection", BindingFlags.Static | BindingFlags.NonPublic);
		}

		static readonly MethodInfo AddItemsToCollectionT;

		private static bool tryGetTry_op(Type type, out MethodInfo mi)
		{
			ParameterInfo[] pis;
			if (op_TryParse.TryGetValue(type, out mi))
				return true;
			else if ((mi = type
							.GetMethods(BindingFlags.Public | BindingFlags.Static)
							.FirstOrDefault(s => s.Name == "TryParse"
												 && s.ReturnType == typeof(bool)
												 && (pis = s.GetParameters()).Length == 2
												 && pis[0].ParameterType == typeof(string)
												 && pis[1].ParameterType == type.MakeByRefType())) != null)
			{
				op_TryParse.Add(type, mi);
				return true;
			}
			else
				return false;
		}

		private static bool tryConvert(Dictionary<Type, MethodInfo> dictMethods, object subj, Func<string> funcSubjToString, Type toType, out object val)
		{
			MethodInfo mi;
			val = null;
			bool b;
			object[] prms;
			if (b = toType.IsEnum)
				val = Enum.Parse(toType, funcSubjToString(), true);
			else if (b = dictMethods.TryGetValue(toType, out mi))
				val = mi.Invoke(null, new[] { subj });
			else if (tryGetTry_op(toType, out mi))
			{
				b = (bool)mi
							.Invoke(null, prms = new object[] { funcSubjToString(), null });
				val = prms[1];
			}
			return b;
		}

		private static bool TryConvert(XElement xe, Type type, out object val)
		{
			return tryConvert(op_XElementConvert, xe, () => xe.Value, type, out val);
		}

		private static bool TryConvertOrDeserialize(XElement xe, Type type, out object val)
		{
			bool b = tryConvert(op_XElementConvert, xe, () => xe.Value, type, out val);
			if (!b)
				b = TryDeserialize(xe, out val, type) || (xe.HasElements && TryDeserialize(xe.Elements().First(), out val, type));
			return b;
		}

		private static bool TryConvert(XAttribute xa, Type type, out object val)
		{
			return tryConvert(op_XAttributeConvert, xa, () => xa.Value, type, out val);
		}
	}
}